/*
 * Copyright 2014 Jonathan Haas
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package app.grinreader.render;

import android.graphics.Canvas;
import android.graphics.RectF;
import android.text.TextPaint;
import android.util.LruCache;
import app.grinreader.model.*;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 *
 */
public class BookPage {
	private final BookModel bookModel;
	private final ExecutorService threadPool;

	private BookPosition startPosition = new BookPosition(0, 0, 0);

	private BookPosition endPosition = new BookPosition(0, 0, 0);

	private ArrayDeque<TextLine> lines = new ArrayDeque<>();

	private final TextPaint textPaint;

	private final RectF pageRectangle;

	private final LruCache<TextBlock, Deque<TextLine>> lineCache = new LruCache<TextBlock, Deque<TextLine>>(1000){
		@Override
		protected Deque<TextLine> create(TextBlock key) {
			return getLinesIntern(key);
		}
	};


	public BookPage(BookModel bookModel, RectF pageRectangle, TextPaint textPaint) {
		this.bookModel = bookModel;
		this.pageRectangle = pageRectangle;
		this.textPaint = textPaint;
		threadPool = Executors.newSingleThreadExecutor();
	}

	public void draw(Canvas canvas) {
		float baselineY = pageRectangle.top;
		for (TextLine line : lines) {
			baselineY += line.getHeight() + line.getMarginTop();
			TextPaint paint = new TextPaint(textPaint);
			float x = pageRectangle.left + line.getIndenting();
			float y = baselineY;
			Element[] elements = line.getText();
			for (Element element : elements) {
				if (element instanceof ContentElement) {
					ContentElement contentElement = (ContentElement) element;
					contentElement.draw(canvas, paint, x, y);
					x+=contentElement.getWidth(paint);
				} else if (element instanceof StyleElement) {
					StyleElement styleElement = (StyleElement) element;
					adjustPaint(paint, styleElement);
					y -= styleElement.getBaselineOffset() * paint.getTextSize();
				} else if (element instanceof ClearStyleElement) {
					paint = new TextPaint(textPaint);
					y = baselineY;
				}
			}
			baselineY += line.getMarginBottom();
		}
	}

	private void adjustPaint(TextPaint paint, StyleElement styleElement) {
		styleElement.adjustPaint(paint);
	}

	public BookPosition getStartPosition() {
		return startPosition;
	}

	public void setStartPosition(BookPosition startPosition) {
		this.startPosition = startPosition;
		reflowLines();
	}

	public BookPosition getEndPosition() {
		return endPosition;
	}

	public void setEndPosition(BookPosition endPosition) {
		this.endPosition = endPosition;
		reflowLinesReverse();
	}

	private void reflowLines() {
		if(pageRectangle.width() <= 0 || pageRectangle.height() <= 0) return;
		BookPosition position = startPosition;
		int totalHeight = 0;
		float maxHeight = pageRectangle.height();
		lines.clear();
		reflowPositive(position, totalHeight, maxHeight);
	}

	private void reflowPositive(BookPosition position, float totalHeight, float maxHeight) {
		TextBlock element = bookModel.getElement(position);
		boolean maySkip = true;
		outer:
		while (element != null) {
			for (TextLine textLine : getLines(element)) {
				if (maySkip && textLine.getEndPosition().getElementOffset() <= position.getElementOffset()) {
					continue;
				}
				if ((!lines.isEmpty() && textLine.isPageBreak()) || totalHeight + textLine.getHeight() + textLine.getMarginTop() > maxHeight) break outer;
				lines.addLast(textLine);
				maySkip = false;
				totalHeight += textLine.getHeight() + textLine.getMarginTop() + textLine.getMarginBottom();
			}
			maySkip = false;
			element = bookModel.getNextElement(element);
		}
		if (lines.size() == 0) {
			this.endPosition = this.startPosition;
		} else {
			this.endPosition = lines.getLast().getEndPosition();
		}
	}

	private void reflowLinesReverse() {
		if(pageRectangle.width() <= 0 || pageRectangle.height() <= 0) return;
		BookPosition position = endPosition;
		float totalHeight = 0;
		float maxHeight = pageRectangle.height();
		lines.clear();
		TextBlock element = bookModel.getElement(position);
		boolean maySkip = true;
		if(position.getElementOffset() == 0){
			element = bookModel.getPreviousElement(element);
			maySkip = false;
		}
		outer:
		while (element != null) {
			for (Iterator<TextLine> iterator = getLines(element).descendingIterator(); iterator.hasNext(); ) {
				TextLine textLine = iterator.next();
				if (maySkip && lines.isEmpty() && textLine.getStartPosition().getElementOffset() >= position.getElementOffset()) {
					continue;
				}
				if (totalHeight + textLine.getHeight() + textLine.getMarginBottom() > maxHeight) break outer;
				lines.addFirst(textLine);
				maySkip = false;
				totalHeight += textLine.getMarginTop() + textLine.getHeight() + textLine.getMarginBottom();
				if(textLine.isPageBreak()){
					break outer;
				}
			}
			maySkip = false;
			element = bookModel.getPreviousElement(element);
		}
		reflowPositive(endPosition, totalHeight, maxHeight);
		if (lines.size() == 0) {
			this.startPosition = this.endPosition;
		} else {
			this.startPosition = lines.getFirst().getStartPosition();
		}
	}

	private Deque<TextLine> getLines(final TextBlock textBlock) {
//		threadPool.submit(new Runnable() {
//			@Override
//			public void run() {
//				try {
//					Thread.sleep(100);
//				} catch (InterruptedException ignored) {
//					return;
//				}
//				TextBlock element = textBlock;
//				for(int i = 0; i < 10; i++){
//					element = bookModel.getNextElement(element);
//					lineCache.get(element);
//				}
//			}
//		});
		return lineCache.get(textBlock);
	}

	private Deque<TextLine> getLinesIntern(TextBlock textBlock) {
		ArrayDeque<TextLine> result = new ArrayDeque<>();
		ArrayList<Element> lineElements = new ArrayList<>();
		ArrayList<StyleElement> styleElements = new ArrayList<>();
		float contentWidth = pageRectangle.width();
		BreakData breakData = new BreakData();
		float currentLineOffset = 0.0f;
		float textIndent = 0.0f;
		float marginTop = 0.0f;
		float marginBottom = 0.0f;
		float marginLeft = 0.0f;
		float marginRight = 0.0f;
		float lineHeight = 0.0f;
		boolean centered = false;
		boolean pageBreak = false;

		Element[] elements;
		elements = textBlock.getElements();
		TextPaint paint = new TextPaint(textPaint);
		int compStart = 0;
		int compEnd = 0;
		for (Element textBlockElement : elements) {
			if (textBlockElement instanceof ContentElement) {
				ContentElement contentElement = (ContentElement) textBlockElement;
				int pos = 0;
				do {
					contentElement.getBreakPosition(contentWidth - currentLineOffset - marginLeft - marginRight, pos, paint, breakData);
					ContentElement subSequence = breakData.getContentBeforeBreak();
					float measure = breakData.getWidth();
					lineHeight = Math.max(lineHeight, contentElement.getHeight(paint));
					currentLineOffset += measure;
					if(compStart == compEnd && breakData.getBreakPos() == 0){
						// Does not fit on line, display anyway
						breakData.setBreakPos(breakData.getEndPos());
						breakData.setContentBeforeBreak(contentElement);
					}
					if(subSequence != null){
						lineElements.add(subSequence);
						compEnd += subSequence.length();
					}
					if (breakData.requireBreak()) {
						float unusedSpace = contentWidth - currentLineOffset -marginLeft -marginRight;
						float leftOffset = textIndent + marginLeft;
						if (centered) {
							leftOffset += unusedSpace * .5f;
							unusedSpace = 0;
						} else if (contentElement instanceof LineBreakElement) {
							unusedSpace = 0; // Do not extend justified text on manual line break
						}
						result.add(
								new TextLine(
										new BookPosition(textBlock.getResourceId(), textBlock.getElementId(), compStart),
										new BookPosition(textBlock.getResourceId(), textBlock.getElementId(), compEnd),
										lineElements.toArray(new Element[lineElements.size()]),
										leftOffset,
										lineHeight,
										unusedSpace,
										currentLineOffset - textIndent,
										paint
								)
						);
						lineElements.clear();
						lineHeight = 0.0f;
						compStart = compEnd;
						currentLineOffset= textIndent = 0;
						// preserve styles for next line
						for (StyleElement styleElement : styleElements) {
							lineElements.add(styleElement);
						}
					}
					pos = breakData.getBreakPos();

				}
				while (pos < contentElement.length());
			} else if (textBlockElement instanceof StyleElement) {
				StyleElement styleElement = (StyleElement) textBlockElement;
				styleElement.adjustPaint(paint);
				styleElements.add(styleElement);
				lineElements.add(styleElement);
				if(compStart == compEnd){
					currentLineOffset = textIndent =styleElement.getTextIndent() * paint.getTextSize();
				}
				marginTop = Math.max(marginTop, styleElement.getMarginTop()* paint.getTextSize());
				marginBottom = Math.max(marginBottom, styleElement.getMarginBottom()* paint.getTextSize());
				marginLeft = Math.max(marginLeft, styleElement.getMarginLeft()* paint.getTextSize());
				marginRight = Math.max(marginRight, styleElement.getMarginRight()* paint.getTextSize());
				centered |= styleElement.isCentered();
				pageBreak |= styleElement.isPageBreak();
			} else if (textBlockElement instanceof ClearStyleElement) {
				paint = new TextPaint(textPaint);
				styleElements.clear();
				lineElements.add(textBlockElement);
			}
		}
		if (compStart != compEnd) {
			float unusedSpace = contentWidth - currentLineOffset -marginLeft -marginRight;
			float leftOffset = textIndent + marginLeft;
			if(centered){
				leftOffset+=unusedSpace*.5f;
			}
			result.add(
					new TextLine(
							new BookPosition(textBlock.getResourceId(), textBlock.getElementId(), compStart),
							new BookPosition(textBlock.getResourceId(), textBlock.getElementId(), compEnd),
							lineElements.toArray(new Element[lineElements.size()]),
							leftOffset,
							lineHeight,
							0,
							currentLineOffset - textIndent,
							paint
					)
			);
		}
		if(result.isEmpty()) return result;
		result.getLast().setExtraContentWidth(0);
		result.getFirst().setMarginTop(marginTop);
		result.getFirst().setPageBreak(pageBreak);
		result.getLast().setMarginBottom(marginBottom);
		return result;
	}
}
